Skip to content

Warn before Sparkle update relaunch kills terminal sessions#1986

Open
austinywang wants to merge 4 commits intomainfrom
issue-1984-session-persist-update
Open

Warn before Sparkle update relaunch kills terminal sessions#1986
austinywang wants to merge 4 commits intomainfrom
issue-1984-session-persist-update

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented Mar 23, 2026

Summary

  • add regression coverage for deferred Sparkle relaunch paths using a two-commit red/green structure
  • guard downloaded/installing update choices and the ready-to-relaunch path so declining the relaunch warning defers instead of immediately continuing
  • guard the auto-update-on-quit restart action so the custom installing state also respects the relaunch warning
  • clear the active user-initiated update presentation when the user declines from the ready-to-install path, avoiding stale update-session state

Notes

  • this is still an incremental mitigation for App update kills all terminal sessions — no session persistence across Sparkle restart #1984, not full terminal session persistence
  • Sparkle still relaunches through normal app termination; this change expands the existing warning so more deferred install paths can be canceled cleanly
  • local tests were not executed per repo policy; the branch uses the required failing-test commit followed by the fix commit so CI can prove the regression

Verification

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -configuration Debug -destination 'platform=macOS' -derivedDataPath /tmp/cmux-issue-1984-session-persist-update-unit build
  • ./scripts/reload.sh --tag issue-1984-session-persist-update

Summary by CodeRabbit

  • New Features

    • Added a confirmation dialog for updates that require app relaunch; shows counts of affected workspaces, local terminal sessions, running commands, and remote/SSH sessions. Update/relaunch proceeds only if the user confirms; otherwise relaunch is deferred.
  • Localization

    • Added English and Japanese localized title and message for the relaunch warning (with placeholders for counts).
  • Tests

    • Added tests covering guarded update install/relaunch behavior and dismissal flows.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Mar 25, 2026 4:52am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 23, 2026

📝 Walkthrough

Walkthrough

Adds a user confirmation step before relaunching the app for updates when active terminal sessions exist, introduces two localized warning strings, a new AppDelegate API to summarize affected terminals and present an NSAlert, and wires the confirmation into the update driver to gate relaunch/install actions.

Changes

Cohort / File(s) Summary
Localization Strings
Resources/Localizable.xcstrings
Added update.relaunchWarning.title and update.relaunchWarning.message with English and Japanese translations; message contains four numeric placeholders (%1$lld%4$lld) for workspace/session/command/remote counts.
AppDelegate: terminal summary & dialog
Sources/AppDelegate.swift
Added UpdateRelaunchTerminalSummary, confirmUpdateRelaunchIfNeeded() -> Bool (public), and updateRelaunchTerminalSummary() helper. Aggregates terminal/workspace/session counts and presents a localized NSAlert with "Restart Now"/"Later" choices.
Update flow integration
Sources/Update/UpdateDriver.swift, Sources/Update/UpdateDelegate.swift
Introduced confirmUpdateRelaunchIfNeededHandler plus builders: makeGuardedUpdateInstallChoiceReply(...) and makeGuardedUpdateRelaunchAction(...). Update presentation and install/relaunch callbacks are wrapped to call the AppDelegate confirmation on the main actor; deferral is logged when user declines.
Tests
cmuxTests/UpdatePillReleaseVisibilityTests.swift
Added UpdateRelaunchGuardTests exercising guarded update-install replies and guarded relaunch actions; includes helpers to construct UpdateDriver and inspect active presentation behavior.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UpdateDriver
    participant AppDelegate
    participant NSAlertUI as NSAlert UI

    UpdateDriver->>AppDelegate: invoke confirmUpdateRelaunchIfNeededHandler()
    AppDelegate->>AppDelegate: updateRelaunchTerminalSummary()
    AppDelegate->>NSAlertUI: present localized warning (title + message with counts)
    NSAlertUI->>User: show dialog ("Restart Now" / "Later")
    User-->>NSAlertUI: choose button
    NSAlertUI-->>AppDelegate: return choice
    alt User chose "Restart Now"
        AppDelegate-->>UpdateDriver: true
        UpdateDriver->>UpdateDriver: proceed with install/relaunch action
    else User chose "Later"
        AppDelegate-->>UpdateDriver: false
        UpdateDriver->>UpdateDriver: log deferred relaunch, skip action
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I counted shells and sessions in a row,
Four tiny numbers in a gentle glow.
"Pause or restart?" I softly plead,
Let users keep their shells and feed,
A little hop to guard their flow.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the main changes and includes verification steps, but is missing key sections from the template: Testing (how manually verified), Demo Video, Review Trigger, and Checklist items. Add Testing section with manual verification details, include Demo Video or link if applicable, add Review Trigger comment block, and complete the Checklist with confirmation of testing and reviews.
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a warning before Sparkle update relaunch kills terminal sessions, which is the core feature across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-1984-session-persist-update
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch issue-1984-session-persist-update

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 23, 2026

Greptile Summary

This PR adds an incremental safety guard against accidental terminal session loss during Sparkle auto-updates. Before cmux terminates to install an update, it now enumerates active TerminalPanel instances across all workspaces, counts running commands and remote sessions, and shows a blocking NSAlert that lets the user defer the restart.

Key changes:

  • AppDelegate.confirmUpdateRelaunchIfNeeded() builds a UpdateRelaunchTerminalSummary and presents an NSAlert; returns false (defer) or true (proceed).
  • AppDelegate.updateRelaunchTerminalSummary() de-duplicates TabManager instances, iterates all workspaces and panels, and returns nil when no terminal sessions are open (suppressing the alert).
  • UpdateDriver.makeGuardedUpdateRelaunchAction() wraps Sparkle's retryTerminatingApplication callback: skips the warning when applicationTerminated == true (old process already gone), shows it otherwise, and dispatches work on @MainActor via Task.
  • New update.relaunchWarning.title / update.relaunchWarning.message localization keys added with English and Japanese translations using positional %lld format specifiers — correctly matched to the String.LocalizationValue interpolation order in the defaultValue.

Minor findings (P2):

  • confirmUpdateRelaunchIfNeeded() lacks @MainActor despite calling NSAlert.runModal() and accessing main-thread state; the current call site is safe but the annotation would provide compiler enforcement.
  • The outer [weak self] capture in the returned closure from makeGuardedUpdateRelaunchAction is unused — only the inner Task captures self.
  • runningCommandCount counts terminal panels with running processes (a Bool per panel), not individual running commands, which may understate impact in the warning dialog.

Confidence Score: 5/5

  • Safe to merge — the warning path is additive and all identified issues are non-blocking P2 style suggestions.
  • The core logic is sound: the applicationTerminated guard, manager deduplication, panel enumeration, and localization format specifiers are all correct. The two call paths (standard Sparkle UI and custom cmux UI) both receive the guarded closure. The three P2 findings are style/clarity improvements (missing @MainActor annotation, redundant outer weak capture, and runningCommandCount label semantics) that don't affect runtime correctness given the current call site always dispatches on @MainActor. No data-loss or regression risk introduced.
  • No files require special attention; all findings are non-blocking P2 suggestions.

Important Files Changed

Filename Overview
Sources/Update/UpdateDriver.swift Wraps the Sparkle retryTerminatingApplication callback with a guarded async action that shows the warning before termination; minor style issue with a redundant outer [weak self] capture.
Sources/AppDelegate.swift Adds confirmUpdateRelaunchIfNeeded() and updateRelaunchTerminalSummary() to enumerate affected workspaces/sessions and show the NSAlert; missing @MainActor annotation on confirmUpdateRelaunchIfNeeded, and runningCommandCount semantics could mislead users.
Resources/Localizable.xcstrings Adds update.relaunchWarning.title and update.relaunchWarning.message with correct positional %lld format specifiers in both English and Japanese translations.

Sequence Diagram

sequenceDiagram
    participant Sparkle
    participant UpdateDriver
    participant AppDelegate
    participant NSAlert

    Sparkle->>UpdateDriver: showInstallingUpdate(applicationTerminated: false, retryTerminatingApplication)
    UpdateDriver->>UpdateDriver: makeGuardedUpdateRelaunchAction(applicationTerminated, action)
    UpdateDriver->>Sparkle: passes guardedRetryTerminatingApplication back

    alt User triggers relaunch (applicationTerminated == false)
        Sparkle->>UpdateDriver: guardedRetryTerminatingApplication()
        UpdateDriver->>UpdateDriver: Task @MainActor
        UpdateDriver->>AppDelegate: confirmUpdateRelaunchIfNeeded()
        AppDelegate->>AppDelegate: updateRelaunchTerminalSummary()
        alt No active terminal sessions
            AppDelegate-->>UpdateDriver: return true
            UpdateDriver->>Sparkle: action()
        else Active terminal sessions found
            AppDelegate->>NSAlert: runModal()
            alt User clicks Restart Now
                NSAlert-->>AppDelegate: alertFirstButtonReturn
                AppDelegate-->>UpdateDriver: return true
                UpdateDriver->>Sparkle: action()
            else User clicks Later
                NSAlert-->>AppDelegate: alertSecondButtonReturn
                AppDelegate-->>UpdateDriver: return false
                Note over UpdateDriver: action() not called — Sparkle waits
            end
        end
    else applicationTerminated == true
        Sparkle->>UpdateDriver: guardedRetryTerminatingApplication()
        UpdateDriver->>UpdateDriver: Task @MainActor
        Note over UpdateDriver: bypass warning
        UpdateDriver->>Sparkle: action() immediately
    end
Loading

Reviews (1): Last reviewed commit: "Warn before update relaunch kills termin..." | Re-trigger Greptile

Comment on lines +471 to +486
{ [weak self] in
Task { @MainActor [weak self] in
guard self != nil else { return }
if applicationTerminated {
action()
return
}

guard AppDelegate.shared?.confirmUpdateRelaunchIfNeeded() ?? true else {
UpdateLogStore.shared.append("update relaunch deferred due to active terminal sessions")
return
}

action()
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant outer [weak self] capture

The outer closure captures [weak self] but never references self directly — only the inner Task closure uses it (and re-captures [weak self] independently). The outer capture has no effect and creates a misleading impression that self is used in the outer scope.

Suggested change
{ [weak self] in
Task { @MainActor [weak self] in
guard self != nil else { return }
if applicationTerminated {
action()
return
}
guard AppDelegate.shared?.confirmUpdateRelaunchIfNeeded() ?? true else {
UpdateLogStore.shared.append("update relaunch deferred due to active terminal sessions")
return
}
action()
}
}
{ in
Task { @MainActor [weak self] in
guard self != nil else { return }
if applicationTerminated {
action()
return
}
guard AppDelegate.shared?.confirmUpdateRelaunchIfNeeded() ?? true else {
UpdateLogStore.shared.append("update relaunch deferred due to active terminal sessions")
return
}
action()
}
}

Comment thread Sources/AppDelegate.swift
updateController.attemptUpdate()
}

func confirmUpdateRelaunchIfNeeded() -> Bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing @MainActor annotation

confirmUpdateRelaunchIfNeeded() calls NSAlert.runModal() (which must run on the main thread) and reads main-thread-only state via updateRelaunchTerminalSummary() (mainWindowContexts, tabManager). The current call site wraps it in Task { @MainActor } correctly, but without the annotation the compiler won't prevent future callers from invoking this method off-actor.

Suggested change
func confirmUpdateRelaunchIfNeeded() -> Bool {
@MainActor
func confirmUpdateRelaunchIfNeeded() -> Bool {

Comment thread Sources/AppDelegate.swift
Comment on lines +6043 to +6050

if workspace.panelNeedsConfirmClose(
panelId: panelId,
fallbackNeedsConfirmClose: terminalPanel.needsConfirmClose()
) {
runningCommandCount += 1
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 runningCommandCount counts panels, not commands

The counter increments once per TerminalPanel whose panelNeedsConfirmClose returns true, but the UI label reads "Running commands detected: N" — implying N individual commands. If a single terminal panel hosts a pipeline of several processes, the number displayed will be lower than the actual running-command count, which could cause users to underestimate the impact of the restart.

Consider renaming the field and label to "Terminal sessions with running commands:" to match what the code actually measures, or documenting the known limitation in a comment.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 3 files

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
Resources/Localizable.xcstrings (1)

77801-77817: Consider adding pluralization for better grammar (optional refinement).

The message currently uses fixed plural forms ("Workspaces affected: %1$lld") which will read slightly awkwardly when counts are 1. As per coding guidelines, cmux prefers the .one/.other key pattern for pluralized strings (e.g., update.relaunchWarning.workspaces.one and update.relaunchWarning.workspaces.other).

However, for a statistics-style warning dialog showing multiple counts, the current implementation is clear and functional. This refinement can be deferred if you prefer to keep the warning message simpler.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Resources/Localizable.xcstrings` around lines 77801 - 77817, The string key
update.relaunchWarning.message uses fixed-count phrases which can be improved by
introducing pluralized keys (e.g., update.relaunchWarning.workspaces.one /
.other, update.relaunchWarning.terminals.one / .other,
update.relaunchWarning.runningCommands.one / .other,
update.relaunchWarning.remoteTerminals.one / .other) and then composing the
final dialog by selecting the correct plural form for each count; update the
Localizable.xcstrings JSON to add these .one/.other entries (with English and
Japanese localizations) and change the dialog construction code to format the
message by inserting the appropriate pluralized fragments instead of the single
update.relaunchWarning.message block so the text reads correctly for singular vs
plural counts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/AppDelegate.swift`:
- Around line 6016-6028: The function updateRelaunchTerminalSummary currently
only appends the active tabManager when managers.isEmpty, which can omit the
active manager if mainWindowContexts is temporarily out of sync; change the
collection logic in updateRelaunchTerminalSummary to always include both sources
(iterate mainWindowContexts and also include the optional tabManager) and then
dedupe by identity (use ObjectIdentifier like the existing seenManagers set) so
every unique TabManager is visited exactly once; reference the symbols
mainWindowContexts, tabManager, seenManagers, and the
updateRelaunchTerminalSummary function and mirror the “visit both sources, then
dedupe by identity” pattern used by forEachTerminalPanel.
- Around line 6044-6048: The code is incrementing runningCommandCount for panels
where workspace.panelNeedsConfirmClose(panelId:fallbackNeedsConfirmClose:)
(which uses panelShellActivityStates in Workspace.swift) is true — this
represents sessions requiring close confirmation, not actual running commands;
change the metric/variable and user-facing text accordingly (e.g., rename
runningCommandCount to sessionsRequiringCloseConfirmationCount and update any
log/message like "Running commands detected" to "Sessions requiring close
confirmation") or, if you need true command counts, replace the increment with
logic that queries the real command enumeration source instead of
terminalPanel.needsConfirmClose() / panelShellActivityStates.
- Around line 5912-5923: The alert.informativeText is using a single localized
paragraph ("update.relaunchWarning.message") with four embedded counts
(summary.workspaceCount, summary.terminalSessionCount,
summary.runningCommandCount, summary.remoteTerminalSessionCount) which prevents
proper plural handling; change to use separate plural-aware localization keys
(e.g. "update.relaunchWarning.workspaces", ".terminalSessions",
".runningCommands", ".remoteTerminalSessions") each with .one/.other forms, then
build alert.informativeText by composing these four localized plural strings
plus the static header lines so each count can be pluralized independently;
update the call-site that constructs alert.informativeText to call
String(localized: "update.relaunchWarning.workspaces", ... ,
summary.workspaceCount) etc., and remove the single combined
"update.relaunchWarning.message" usage.

---

Nitpick comments:
In `@Resources/Localizable.xcstrings`:
- Around line 77801-77817: The string key update.relaunchWarning.message uses
fixed-count phrases which can be improved by introducing pluralized keys (e.g.,
update.relaunchWarning.workspaces.one / .other,
update.relaunchWarning.terminals.one / .other,
update.relaunchWarning.runningCommands.one / .other,
update.relaunchWarning.remoteTerminals.one / .other) and then composing the
final dialog by selecting the correct plural form for each count; update the
Localizable.xcstrings JSON to add these .one/.other entries (with English and
Japanese localizations) and change the dialog construction code to format the
message by inserting the appropriate pluralized fragments instead of the single
update.relaunchWarning.message block so the text reads correctly for singular vs
plural counts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d94cbce3-217d-458f-b1c3-50aa2ce5a8e4

📥 Commits

Reviewing files that changed from the base of the PR and between 88751b2 and fa7c166.

📒 Files selected for processing (3)
  • Resources/Localizable.xcstrings
  • Sources/AppDelegate.swift
  • Sources/Update/UpdateDriver.swift

Comment thread Sources/AppDelegate.swift
Comment on lines +5912 to +5923
alert.informativeText = String(
localized: "update.relaunchWarning.message",
defaultValue: """
This update will relaunch cmux and terminate in-app terminal sessions.

Workspaces affected: \(summary.workspaceCount)
Terminal sessions affected: \(summary.terminalSessionCount)
Running commands detected: \(summary.runningCommandCount)
Remote / SSH terminal sessions: \(summary.remoteTerminalSessionCount)

Shells, SSH connections, and agents attached to these PTYs will be terminated.
"""
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use plural-aware localized strings for the count lines.

This packs four counted labels into one fixed-plural paragraph, so translators cannot independently handle 1 vs n for each line. Split these into separate .one / .other resources (or otherwise plural-aware localized lines) before composing the alert body.

Based on learnings: In Swift files (cmux project), when handling pluralized strings, prefer using localization keys with the ICU-style plural forms .one and .other.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 5912 - 5923, The
alert.informativeText is using a single localized paragraph
("update.relaunchWarning.message") with four embedded counts
(summary.workspaceCount, summary.terminalSessionCount,
summary.runningCommandCount, summary.remoteTerminalSessionCount) which prevents
proper plural handling; change to use separate plural-aware localization keys
(e.g. "update.relaunchWarning.workspaces", ".terminalSessions",
".runningCommands", ".remoteTerminalSessions") each with .one/.other forms, then
build alert.informativeText by composing these four localized plural strings
plus the static header lines so each count can be pluralized independently;
update the call-site that constructs alert.informativeText to call
String(localized: "update.relaunchWarning.workspaces", ... ,
summary.workspaceCount) etc., and remove the single combined
"update.relaunchWarning.message" usage.

Comment thread Sources/AppDelegate.swift
Comment on lines +6016 to +6028
private func updateRelaunchTerminalSummary() -> UpdateRelaunchTerminalSummary? {
var seenManagers = Set<ObjectIdentifier>()
var managers: [TabManager] = []

for context in mainWindowContexts.values {
let managerId = ObjectIdentifier(context.tabManager)
guard seenManagers.insert(managerId).inserted else { continue }
managers.append(context.tabManager)
}

if managers.isEmpty, let tabManager {
managers.append(tabManager)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Do not hide the active tabManager behind an empty-array fallback.

This only appends tabManager when mainWindowContexts is empty. If window/context bookkeeping is temporarily out of sync, the active manager's terminals can be omitted from the summary and the relaunch warning can undercount or disappear. The existing forEachTerminalPanel helper later in this file already uses the safer “visit both sources, then dedupe by identity” pattern.

♻️ Suggested adjustment
         for context in mainWindowContexts.values {
             let managerId = ObjectIdentifier(context.tabManager)
             guard seenManagers.insert(managerId).inserted else { continue }
             managers.append(context.tabManager)
         }

-        if managers.isEmpty, let tabManager {
-            managers.append(tabManager)
+        if let tabManager {
+            let managerId = ObjectIdentifier(tabManager)
+            if seenManagers.insert(managerId).inserted {
+                managers.append(tabManager)
+            }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private func updateRelaunchTerminalSummary() -> UpdateRelaunchTerminalSummary? {
var seenManagers = Set<ObjectIdentifier>()
var managers: [TabManager] = []
for context in mainWindowContexts.values {
let managerId = ObjectIdentifier(context.tabManager)
guard seenManagers.insert(managerId).inserted else { continue }
managers.append(context.tabManager)
}
if managers.isEmpty, let tabManager {
managers.append(tabManager)
}
private func updateRelaunchTerminalSummary() -> UpdateRelaunchTerminalSummary? {
var seenManagers = Set<ObjectIdentifier>()
var managers: [TabManager] = []
for context in mainWindowContexts.values {
let managerId = ObjectIdentifier(context.tabManager)
guard seenManagers.insert(managerId).inserted else { continue }
managers.append(context.tabManager)
}
if let tabManager {
let managerId = ObjectIdentifier(tabManager)
if seenManagers.insert(managerId).inserted {
managers.append(tabManager)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 6016 - 6028, The function
updateRelaunchTerminalSummary currently only appends the active tabManager when
managers.isEmpty, which can omit the active manager if mainWindowContexts is
temporarily out of sync; change the collection logic in
updateRelaunchTerminalSummary to always include both sources (iterate
mainWindowContexts and also include the optional tabManager) and then dedupe by
identity (use ObjectIdentifier like the existing seenManagers set) so every
unique TabManager is visited exactly once; reference the symbols
mainWindowContexts, tabManager, seenManagers, and the
updateRelaunchTerminalSummary function and mirror the “visit both sources, then
dedupe by identity” pattern used by forEachTerminalPanel.

Comment thread Sources/AppDelegate.swift
Comment on lines +6044 to +6048
if workspace.panelNeedsConfirmClose(
panelId: panelId,
fallbackNeedsConfirmClose: terminalPanel.needsConfirmClose()
) {
runningCommandCount += 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

This is a busy-session count, not a command count.

This increments once per terminal panel based on panelNeedsConfirmClose(...); Sources/Workspace.swift:6074-6079 backs that with panelShellActivityStates, so the downstream “Running commands detected” wording reads more precise than the data you actually have. Rename the metric/message to something like “sessions requiring close confirmation,” or switch to a real command enumeration source.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 6044 - 6048, The code is incrementing
runningCommandCount for panels where
workspace.panelNeedsConfirmClose(panelId:fallbackNeedsConfirmClose:) (which uses
panelShellActivityStates in Workspace.swift) is true — this represents sessions
requiring close confirmation, not actual running commands; change the
metric/variable and user-facing text accordingly (e.g., rename
runningCommandCount to sessionsRequiringCloseConfirmationCount and update any
log/message like "Running commands detected" to "Sessions requiring close
confirmation") or, if you need true command counts, replace the increment with
logic that queries the real command enumeration source instead of
terminalPanel.needsConfirmClose() / panelShellActivityStates.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
Sources/AppDelegate.swift (2)

6025-6033: ⚠️ Potential issue | 🟠 Major

Always include the current tabManager in this scan.

Line 6031 only appends tabManager when mainWindowContexts is empty. If that registry is momentarily stale, terminalSessionCount can fall to 0 and this warning path gets skipped even though live terminals still exist. Mirror forEachTerminalPanel(_:) and visit both sources, then dedupe by ObjectIdentifier.

♻️ Suggested adjustment
         for context in mainWindowContexts.values {
             let managerId = ObjectIdentifier(context.tabManager)
             guard seenManagers.insert(managerId).inserted else { continue }
             managers.append(context.tabManager)
         }
 
-        if managers.isEmpty, let tabManager {
-            managers.append(tabManager)
+        if let tabManager {
+            let managerId = ObjectIdentifier(tabManager)
+            if seenManagers.insert(managerId).inserted {
+                managers.append(tabManager)
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 6025 - 6033, The scan over window
contexts currently only appends the current tabManager when no managers were
found, which can miss live terminals if mainWindowContexts is stale; modify the
logic that builds managers (the loop over mainWindowContexts and the conditional
that appends tabManager) so you always visit both sources—iterate
mainWindowContexts.values and also visit the current tabManager (if non-nil) and
use ObjectIdentifier + seenManagers.insert(...) to dedupe exactly as is done for
the contexts (mirror forEachTerminalPanel(_:)'s approach), ensuring tabManager
is added when not already seen rather than only when managers.isEmpty.

5917-5929: ⚠️ Potential issue | 🟡 Minor

Split these counts into plural-aware localized lines.

Line 5917 folds four counted labels into one localized paragraph, so each line cannot get its own singular/plural form. Use separate .one / .other keys for the counted lines and compose the alert body from those localized pieces. Based on learnings: prefer .one / .other localization keys for pluralized Swift UI strings.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 5917 - 5929, The
alert.informativeText currently uses one localized paragraph with embedded
counts, so pluralization can't vary per count; instead create separate
plural-aware localized strings for each counted line (use String(localized:
"update.relaunchWarning.workspaces", table:nil, count: summary.workspaceCount)
with .one/.other keys, similarly for terminalSessionCount, runningCommandCount,
and remoteTerminalSessionCount), then build alert.informativeText by
concatenating the static header and these four localized lines (referencing
alert.informativeText, summary.workspaceCount, summary.terminalSessionCount,
summary.runningCommandCount, summary.remoteTerminalSessionCount) so each count
uses proper singular/plural localization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/AppDelegate.swift`:
- Around line 6025-6033: The scan over window contexts currently only appends
the current tabManager when no managers were found, which can miss live
terminals if mainWindowContexts is stale; modify the logic that builds managers
(the loop over mainWindowContexts and the conditional that appends tabManager)
so you always visit both sources—iterate mainWindowContexts.values and also
visit the current tabManager (if non-nil) and use ObjectIdentifier +
seenManagers.insert(...) to dedupe exactly as is done for the contexts (mirror
forEachTerminalPanel(_:)'s approach), ensuring tabManager is added when not
already seen rather than only when managers.isEmpty.
- Around line 5917-5929: The alert.informativeText currently uses one localized
paragraph with embedded counts, so pluralization can't vary per count; instead
create separate plural-aware localized strings for each counted line (use
String(localized: "update.relaunchWarning.workspaces", table:nil, count:
summary.workspaceCount) with .one/.other keys, similarly for
terminalSessionCount, runningCommandCount, and remoteTerminalSessionCount), then
build alert.informativeText by concatenating the static header and these four
localized lines (referencing alert.informativeText, summary.workspaceCount,
summary.terminalSessionCount, summary.runningCommandCount,
summary.remoteTerminalSessionCount) so each count uses proper singular/plural
localization.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9357ec21-19ad-4bca-abe5-790f8874f028

📥 Commits

Reviewing files that changed from the base of the PR and between fa7c166 and 53e3591.

📒 Files selected for processing (1)
  • Sources/AppDelegate.swift

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
Sources/Update/UpdateDriver.swift (1)

488-508: Consider making the helper internal visibility explicit.

Both makeGuardedUpdateInstallChoiceReply and makeGuardedUpdateRelaunchAction lack access modifiers, defaulting to internal. This is correct for testability (the test file uses them), but adding explicit internal would document the intent.

📝 Suggested clarity improvement
-    func makeGuardedUpdateInstallChoiceReply(
+    internal func makeGuardedUpdateInstallChoiceReply(
         _ reply: `@escaping` `@Sendable` (SPUUserUpdateChoice) -> Void
     ) -> `@Sendable` (SPUUserUpdateChoice) -> Void {
-    func makeGuardedUpdateRelaunchAction(
+    internal func makeGuardedUpdateRelaunchAction(
         applicationTerminated: Bool,
         action: `@escaping` () -> Void
     ) -> () -> Void {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Update/UpdateDriver.swift` around lines 488 - 508, Add the explicit
internal access modifier to the helper functions to document intended test
visibility: change the declarations of makeGuardedUpdateInstallChoiceReply(_:)
and makeGuardedUpdateRelaunchAction(_:) to start with "internal func"
(preserving their existing signatures, attributes like `@Sendable`, and capture
semantics) so their visibility is explicit for readers and tests.
cmuxTests/UpdatePillReleaseVisibilityTests.swift (2)

302-310: Mirror-based introspection is fragile but acceptable for tests.

This approach accesses a private property via reflection. If the property name changes in UpdateDriver, this helper will silently return nil. Consider adding a comment noting this coupling, or alternatively exposing a test-only accessor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/UpdatePillReleaseVisibilityTests.swift` around lines 302 - 310, The
test helper activePresentation uses mirror-based reflection to access
UpdateDriver's private property "activeUserInitiatedCheckPresentation", which is
fragile if that property is renamed; either add a clear comment above the
activePresentation(on:) helper noting this coupling and the risk (mentioning
UpdateDriver and the "activeUserInitiatedCheckPresentation" field), or
preferably add a test-only accessor on UpdateDriver (e.g., internal or `@testable`
var activeUserInitiatedCheckPresentation) and update activePresentation(on:) to
use that accessor instead of Mirror so the test is robust to refactors.

235-270: Consider adding a test for applicationTerminated: true scenario.

The current tests cover applicationTerminated: false, but makeGuardedUpdateRelaunchAction has a different code path when applicationTerminated: true (it skips confirmation and runs the action immediately). Adding a test would ensure that path is covered.

🧪 Suggested additional test
func testGuardedRelaunchActionRunsImmediatelyWhenApplicationTerminated() async {
    var confirmationCallCount = 0
    let driver = makeDriver {
        confirmationCallCount += 1
        return false // Would block if checked
    }
    let actionExpectation = expectation(description: "relaunch action invoked")

    let guardedAction = driver.makeGuardedUpdateRelaunchAction(applicationTerminated: true) {
        actionExpectation.fulfill()
    }

    guardedAction()

    await fulfillment(of: [actionExpectation], timeout: 1.0)
    XCTAssertEqual(confirmationCallCount, 0, "Confirmation should not be checked when app is already terminated")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/UpdatePillReleaseVisibilityTests.swift` around lines 235 - 270, Add
a new async unit test for the applicationTerminated: true branch of
makeGuardedUpdateRelaunchAction that verifies the action runs immediately and
the confirmation callback is not invoked: create a driver whose confirmation
closure increments a counter and returns false, create an expectation that the
relaunch action is invoked, call
driver.makeGuardedUpdateRelaunchAction(applicationTerminated: true) { fulfill
expectation }, invoke the returned guardedAction(), await the expectation
(timeout ~1s) and assert confirmationCallCount == 0 to ensure confirmation was
skipped.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmuxTests/UpdatePillReleaseVisibilityTests.swift`:
- Around line 302-310: The test helper activePresentation uses mirror-based
reflection to access UpdateDriver's private property
"activeUserInitiatedCheckPresentation", which is fragile if that property is
renamed; either add a clear comment above the activePresentation(on:) helper
noting this coupling and the risk (mentioning UpdateDriver and the
"activeUserInitiatedCheckPresentation" field), or preferably add a test-only
accessor on UpdateDriver (e.g., internal or `@testable` var
activeUserInitiatedCheckPresentation) and update activePresentation(on:) to use
that accessor instead of Mirror so the test is robust to refactors.
- Around line 235-270: Add a new async unit test for the applicationTerminated:
true branch of makeGuardedUpdateRelaunchAction that verifies the action runs
immediately and the confirmation callback is not invoked: create a driver whose
confirmation closure increments a counter and returns false, create an
expectation that the relaunch action is invoked, call
driver.makeGuardedUpdateRelaunchAction(applicationTerminated: true) { fulfill
expectation }, invoke the returned guardedAction(), await the expectation
(timeout ~1s) and assert confirmationCallCount == 0 to ensure confirmation was
skipped.

In `@Sources/Update/UpdateDriver.swift`:
- Around line 488-508: Add the explicit internal access modifier to the helper
functions to document intended test visibility: change the declarations of
makeGuardedUpdateInstallChoiceReply(_:) and makeGuardedUpdateRelaunchAction(_:)
to start with "internal func" (preserving their existing signatures, attributes
like `@Sendable`, and capture semantics) so their visibility is explicit for
readers and tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7bdebc0f-fa38-4ec8-87b9-d9e4392f5294

📥 Commits

Reviewing files that changed from the base of the PR and between 53e3591 and d481640.

📒 Files selected for processing (3)
  • Sources/Update/UpdateDelegate.swift
  • Sources/Update/UpdateDriver.swift
  • cmuxTests/UpdatePillReleaseVisibilityTests.swift

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants